Ruby tutorials

What is Ruby?

Ruby is an open-source object-oriented scripting language invented in the mid-90s by Yukihiro Matsumoto. Unlike languages such as C and C++, a scripting language doesn’t talk directly to hardware. Programs are generally procedural in nature, meaning they are read from top to bottom. Object-oriented languages, on the other hand, break out pieces of code into objects that can be created and used as needed. You can reuse these objects in other parts of the program, or even other applications.

What can Ruby be used for?

The Ruby programming language is a highly portable general-purpose language that serves many purposes. Ruby is great for building desktop applications, static websites, data processing services, and even automation tools. It’s used for web servers, DevOps, and web scraping and crawling. And when you add in the functionality of the Rails application framework, you can do even more, especially database-driven web applications.

Ruby on Rails

Ruby stands alone as a high-level programming language. But you really can't talk about Ruby without mentioning Rails. Ruby on Rails is the application framework that thrust Ruby into the spotlight, boosted its popularity, and made it a great language for the cloud. The Ruby on Rails framework consists of pre-written Ruby code for things like communication, file handling, database connections, and more. It takes care of the tedious items, so you can focus on solving problems. One of the key concepts of Rails is DRY — Don’t Repeat Yourself — which is key to the framework’s efficiency. There are over a million websites written in Ruby on Rails.

Ruby vs. Python

Ruby and Python have a lot in common. Both Ruby and Python are high-level server-side scripting languages with clear and easy-to-read syntax, but there are some important technical differences.

Differences between Ruby vs Python

Some of the differences between Ruby vs Python include: Python supports multiple IDEs, whereas Ruby supports only EclipseIDE. With Python you’re limited to the Django framework; with Ruby, you’re limited to Rails. Ruby uses a powerful blocks feature, but Python offers more libraries. Ruby is a true object-oriented language, but Python has more traction among data scientists. And on it goes, tit for tat. Then there are the more subtle differences, too. Some find Python easier to learn initially but more stifling in the long run. In many ways, it boils down to a basic philosophical difference between the two: In Ruby, there are many ways of doing things, many solutions to one problem. In Python, there’s a best way of doing things, and that’s the way you should do it.

Why should I learn Ruby?

The Ruby programming language is designed for programmer productivity and fun. Developers like using Ruby because it’s high level and has a simple syntax. You have less code to write and can focus on finding a solution to your problem. While many low-level languages require lines and lines of code for the smallest thing, with Ruby, you can write your first cloud application in just a few hours. Ruby is "A Programmer's Best Friend". Ruby has features that are similar to those of Smalltalk, Perl, and Python. Perl, Python, and Smalltalk are scripting languages. Smalltalk is a true object-oriented language. Ruby, like Smalltalk, is a perfect object-oriented language. Using Ruby syntax is much easier than using Smalltalk syntax. Features of Ruby Ruby is an open-source and is freely available on the Web, but it is subject to a license. Ruby is a general-purpose, interpreted programming language. Ruby is a true object-oriented programming language. Ruby is a server-side scripting language similar to Python and PERL. Ruby can be used to write Common Gateway Interface (CGI) scripts. Ruby can be embedded into Hypertext Markup Language (HTML). Ruby has a clean and easy syntax that allows a new developer to learn very quickly and easily. Ruby has similar syntax to that of many programming languages such as C++ and Perl. Ruby is very much scalable and big programs written in Ruby are easily maintainable. Ruby can be used for developing Internet and intranet applications. Ruby can be installed in Windows and POSIX environments. Ruby support many GUI tools such as Tcl/Tk, GTK, and OpenGL. Ruby can easily be connected to DB2, MySQL, Oracle, and Sybase. Ruby has a rich set of built-in functions, which can be used directly into Ruby scripts.

Sum Of Two Numbers

In this example we want to find out if given an array of unique numbers, there is a combination of two numbers which adds up to a target number. Code: def sum_eq_n?(arr, n) return true if arr.empty? && n == 0 arr.product(arr).reject { |a,b| a == b }.any? { |a,b| a + b == n } end This is interesting because I'm using the product method here. When you use this method is like having a loop inside a loop that combines all values in array A with all values in array B.

Counting, Mapping & Finding

Let's say that you want to find the missing number in an arithmetic sequence, like (2,4,6,10). We can use a strategy where we calculate the difference between the numbers. [2, 2, 4] Our goal here is to find out what the sequence is. Is it increasing or decreasing? By how much? This code reveals the sequence: differences = [2, 2, 4] differences.max_by { |n| differences.count(n) } # 2 # This is the increase between numbers in the sequence Once we know the sequence we can compare all the numbers to find the missing one. Here's the code: def find_missing(sequence) consecutive = sequence.each_cons(2) differences = consecutive.map { |a,b| b - a } sequence = differences.max_by { |n| differences.count(n) } missing_between = consecutive.find { |a,b| (b - a) != sequence } missing_between.first + sequence end find_missing([2,4,6,10]) # 8 Count how many Ruby methods we're using here to do the hard work for us 🙂

Regular Expression Example

If you are working with strings & you want to find patterns then regular expressions are your friend. They can be a bit tricky to get right, but practice makes mastery! Now: Let's say that we want to find out if a given string follows a pattern of VOWEL to NON-VOWEL characters. Like this: "ateciyu" Then we can use a regular expression, along with the match? method to figure this out. Here's the code example: def alternating_characters?(s) type = [/[aeiou]/, /[^aeiou]/].cycle if s.start_with?(/[^aeiou]/) type.next end s.chars.all? { |ch| ch.match?(type.next) } end alternating_characters?("ateciyu") # true Notice a few things: We use the cycle method so we can keep switching between the VOWEL regex & the NON-VOWEL regex. We convert the string into an array of characters with chars so that we can use the all? method.

Recursion & Stack Example

Recursion is when a method calls itself multiple times as a way to make progress towards a solution. Many interesting problems can be solved with recursion. But because recursion has its limits, you can use a stack data structure instead. Now: Let's look at an example where we want to find out the “Power Set” of a given array. The Power Set is a set of all the subsets that can be created from the array. Here's an example with recursion: def get_numbers(list, index = 0, taken = []) return [taken] if index == list.size get_numbers(list, index+1, taken) + get_numbers(list, index+1, taken + [list[index]]) end get_numbers([1,2,3]) Here's the same problem solved using a stack: def get_numbers_stack(list) stack = [[0, []]] output = [] until stack.empty? index, taken = stack.pop next output << taken if index == list.size stack.unshift [index + 1, taken] stack.unshift [index + 1, taken + [list[index]]] end output end The idea here is that on each pass of the algorithm we are either taking a number or not taking a number. We branch out & try both outcomes so we can produce all the possible combinations. Imagine a tree where each leaf is one of the solutions. A few things to notice: The recursion solution is shorter The actual "making progress" part of the algorithm (index + 1) is almost the same The stack we're using is just an array because there isn't a Stack class in Ruby

Method Chaining Example

This is my favorite example because it shows how powerful Ruby is. Combining methods allows you to take the output produced by one method & pass it into another. Just like a factory production line! You start with some raw materials (input), then through the process of calling these methods, you slowly transform the raw materials into the desired result. Here's an example: def longest_repetition(string) max = string .chars .chunk(&:itself) .map(&:last) .max_by(&:size) max ? [max[0], max.size] : ["", 0] end longest_repetition("aaabb") # ["a", 3] Given a string, this code will find the longest repeated character. Note: How this code is formatted to maximize readability Use of the Symbol#to_proc pattern (&:size) Btw, don't confuse this with the "Law of Demeter". That "law" is about reaching out into the internals of another object. Here we are only transforming objects.

With Index Example

Would you like to have the current index while iterating over a collection of items? You can use the with_index method. Here's an example: def reverse_alternate(string) string.gsub(/[^\s]+/).with_index { |w, idx| idx.even? ? w : w.reverse } end reverse_alternate("Apples Are Good") # "Apples erA Good" Notice: We combine with_index & even? to find if we have to reverse the current word Gsub without a block returns an Enumerator object, which allows you to chain it with other methods

Each With Object Example

Another interesting method is each_with_object, and its friend with_object. You can use these two methods when you need an object to hold the results. Here's an example: def clean_string(str) str .chars .each_with_object([]) { |ch, obj| ch == "#" ? obj.pop : obj << ch } .join end clean_string("aaa#b") In this example, we want to delete the last character when we find a # symbol. Notice: each_with_object takes an argument, which is the object we want to start with. This argument becomes the 2nd block parameter. We are converting the string into an array of characters (char) & then back to a string when we are done (join). We are using a ternary operator to decide what to do, this makes the code shorter.

Ruby Hello, World!

print "Hello, World!\n" The Ruby Hello, World! program is a one-liner. C-like (really perl-like) string constant No semicolon needed (though it wouldn't break anything). # Variables and expressions. a = 10 b = 3 * a + 2 printf("%d %d\n", a, b); # Type is dynamic. b = "A string" c = 'Another String' print b + " and " + c + "\n" The variables shown in this example are "local variables". (We'll explore other possibilities later.) Names are mostly conventional: letters, underscore, and digit, not starting with a digit. Local variables must start with a lower-case letter. Variable types are dynamic: Assignment transfers both and type, not just value. Variables are not declared with a type, and assignment does not need to perform conversion. Obviously, the + concatenates strings, as in Java and C++. The printf function is lifted from plain C.

Objects

# Operators are really method invocations. a = 10 b = 3.*(a).+(2) Kernel::printf("%d %d\n", a, b); # Type is still dynamic. b = String.new("A string") c = 'Another String' Kernel.print(b.+(" and ")::+(c).+("\n")) Here is a more honest version of the last example. Ruby is a fairly pure object-oriented language, which borrows a good bit from Smalltalk. Pretty much everything is an object. Ruby has a bit more complex syntax than Smalltalk, some of it designed to make much of Ruby look more conventional. But it's invoking methods on objects just the same. For instance, ordinary integers are objects of the class Fixnum. The numeric operations are methods. Ruby lets you write 3 * a, but what you're really doing is invoking the * method on the object 3, and you can write it that way 3.*(a). The name Kernel refers to a built-in module. A module is similar to a static class in Java. The builtin-in functions print and printf are part of Kernel. As far as I can tell, the :: and . operators are equivalent. Of course, string constants are objects of class String, and this is made explicit in the statement b = String.new("A String");. Notice that new is a method of class String, rather than being an operator as in Java and C++. We'll usually use the more conventional syntax, but the heavy use of objects provides gread flexibilty. We'll flex later. Also note that there is no sense of Java's wrapper classes, like Integer. When you write an integer constant, it's an object of Ruby's Integer class. There is no int.

Strings

# Double-quoted strings can substitute variables. a = 17 print "a = #{a}\n"; print 'a = #{a}\n'; print "\n"; # If you're verbose, you can create a multi-line string like this. b = <<ENDER This is a longer string, perhaps some instructions or agreement goes here. By the way, a = #{a}. ENDER print "\n[[[" + b + "]]]\n"; print "\nActually, any string can span lines. The line\nbreaks just become part of the string. " print %Q=\nThe highly intuitive "%Q" prefix allows alternative delimiters.\n= print %Q[Bracket symbols match their mates, not themselves.\n] Ruby has many ways of making strings, which are generally variations of the many ways perl has of making strings. Double quotes allow values to be interpolated into the string, while single quotes do not. Most escapes are treated literally in single quotes, including the fact that \n is treated as two characters, a backward slash followed by an n. Ruby strings may extend any number of lines. This can be useful, but make sure you don't leave out any closing quotes. The << notation follows perl and other languages as a way to multi-line strings. Those don't generally permit the other varieties to specify multi-line strings, so it's actually kind of redundant in Ruby. The % can be used to create strings using a different delimiter. %Qx starts a double-quote style string which ends with the next x, rather than the usual double quote character. %qy starts a single-quote string.

String Operations

s = "Hi there. How are you?" print s.length, " [" + s + "]\n" # Selecting a character in a string gives an integer ascii code. print s[4], "\n" printf("%c\n", s[4]) # The [n,l] substring gives the starting position and length. The [n..m] # form gives a range of positions, inclusive. print "[" + s[4,4] + "] [" + s[6..15] + "]\n" print "Wow " * 3, "\n" print s.index("there"), " ", s.index("How"), " ", s.index("bogus"), "\n" print s.reverse, "\n"

Arrays

a = [ 45, 3, 19, 8 ] b = [ 'sam', 'max', 56, 98.9, 3, 10, 'jill' ] print (a + b).join(' '), "\n" print a[2], " ", b[4], " ", b[-2], "\n" print a.sort.join(' '), "\n" a << 57 << 9 << 'phil' print "A: ", a.join(' '), "\n" b << 'alex' << 48 << 220 print "B: ", b.join(' '), "\n" print "pop: ", b.pop, "\n" print "shift: ", b.shift, "\n" print "C: ", b.join(' '), "\n" b.delete_at(2) b.delete('alex') print "D: ", b.join(' '), "\n" Ruby arrays have operations similar to strings. As the quote notation creates a String object, the bracket notation creates an Array object.

Hashes

z = { 'mike' => 75, 'bill' => 18, 'alice' => 32 } z['joe'] = 44 print z['bill'], " ", z['joe'], " ", z["smith"], "\n" print z.has_key?('mike'), " ", z.has_key?("jones"), "\n" Ruby hashes are similar to maps or dictionaries in other languages. They are essentially arrays whose subscripts are not limited to integer values. You can create them with the curly-bracked list notation shown, but you can also assign to a subscript expression to add members. It's not at all unreasonable to create a hash with empty brackets, then add members using subscripting. Method names may end in ?, hence the name of the has_key? method. This suggests a the method is a test which returns a boolean value. Such methods are sometimes known as predicates.

Line Breaking

a = 10 b = a + 10 c = [ 5, 4, 10 ] d = [ a ] \ + c print "#{a} #{b} [", c.join(" "), "] [", d.join(" "), "]\n"; Ruby generally uses line breaks instead of semicolons to separate statements. Lines can be continued by using a \ at the end, but this is rarely needed. If the line ends with pretty much anything that suggests there should be more, Ruby will continue on to the next line. This includes such things as ending with an operator, or inside parentheses or something else which needs to be closed.

Basic I/O I

# Get the parts of speech print "Please enter a past-tense verb: " verb = gets.chomp print "Please enter a noun: " noun = gets.chomp print "Please enter a proper noun: " prop_noun = gets.chomp print "Please enter a an adverb: " adv = gets.chomp # Make the sentence. print "#{prop_noun} got a #{noun} and\n#{verb} #{adv} around the block.\n"
See:Ruby Manual
The built-in gets function simply reads a line and returns it as a string. The chomp method from trims the line terminator. The prompts are generated with ordinary prints lacking a final newline.

Basic I/O II

print "Triangle height: " h = gets.to_f; print "Triangle width: " w = gets.to_f; area = 0.5*h*w print "Triangle height ", h, " width ", w, " has area ", area, "\n"
See:Ruby Manual
Similar to the last one, the to_f method of converts the input string to a floating-point number.

File I/O

# Get the parts of speech. print "Please enter a past-tense verb: " verb = gets.chomp print "Please enter a noun: " noun = gets.chomp print "Please enter a proper noun: " prop_noun = gets.chomp print "Please enter a an adverb: " adv = gets.chomp # See where to put it. print "Please enter a file name: " fn = gets.chomp handle = open(fn,"w") # Go. printf(handle, "%s got a %s and\n%s %s around the block.\n", prop_noun, noun, verb, adv) handle.close
See:Ruby Manual
This version opens a file for writing, and writes its output to a file selected by the user. The second argument to open is the same as the second argument to a plain C fopen, except it will default to reading. The object returned from open is .

Parallel Asst.

# Assign three values. a, b, c = 8, 10, 15 print "A: a = ", a, ", b = ", b, ", c = ", c, "\n" # Compute three values, then assign three values. a, b, c = 40, a + 11, a + b + c print "B: a = ", a, ", b = ", b, ", c = ", c, "\n" # Swap. a, b = b, a print "C: a = ", a, ", b = ", b, ", c = ", c, "\n" # Extras on left get nil. a, b, c = 2, 3 print "D: a = ", a, ", b = ", b, ", c = ", c, "\n" # Extras on right get left behind a, b, c = 11, 12, 13, 14, 15 print "E: a = ", a, ", b = ", b, ", c = ", c, "\n" # The right can be an array, in which case the members are assigned to # individual variables. fred = [ 4, 5, 6, 7] a, b, c = fred print "F: a = ", a, ", b = ", b, ", c = ", c, "\n" A parallel assignment sets several variables at once. All right sides are computed before any variable is assigned, so all right side values are computed with the old variable values. This is very convenient for swapping two values without using a temporary. When the right side is an array, the members of the array are broken out and assigned to individual variables.

Conditional I

# Pick a random number. rno = rand(100) + 1 print "Your magic number is ", rno, "\n" # Perform all sort of totally uselss test on it and report the results. if rno % 2 == 1 then print "Ooooh, that's an odd number.\n" else print "That's an even number.\n" if rno > 2 then print "It's not prime, BTW.\n" end end if rno > 50 print "That's more than half as big as it could be!\n" elsif rno == 42 print "That's the ultimate magic number!!!!\n" elsif rno < 10 print "That's pretty small, actually.\n" else print "What a boring number.\n" end if rno == 100 then print "Gosh, you've maxxed out!\n" end This is the conventional use of the if in perl. Notice that there is no need for curly braces ({ and }). The body of the if ends with the appropriate keyword, end, else or elsif. The then word is generally optional, though you need it if you want to put start the body on the same line as the if, the way the last statement does. As in most languages (excluding Python), indenting is not required, and is ignored by the interpreter. Of course, it is wise to indent in a way which reflects the structure of the code.

Conditional II

# Let the user guess. print "Enter heads or tails? " hort = gets.chomp unless hort == 'heads' || hort == 'tails' print "I _said_ heads or tails. Can't you read?\n" exit(1) end # Now toss the coin. toss = if rand(2) == 1 then "heads" else "tails" end # Report. print "Toss was ", toss, ".\n" print "You Win!\n" if hort == toss Some more amazing if lore:
    The unless keyword is like if, but uses the opposite sense of the test: The code is run if the test is false. As with many things in Ruby, if may look like a statement, but it is actually an expression, with a return value. The if has a postfix form where the condition comes last. This works with unless as well.

While Loop

# Some counting with a while. a = 0 while a < 15 print a, " " if a == 10 then print "made it to ten!!" end a = a + 1 end print "\n" # Here's a way to empty an array. joe = [ 'eggs.', 'some', 'break', 'to', 'Have' ] print(joe.pop, " ") while joe.size > 0 print "\n" The while loop is conventional, but it also has a postfix form.

For Loop

# Simple for loop using a range. for i in (1..4) print i," " end print "\n" for i in (1...4) print i," " end print "\n" # Running through a list (which is what they do). items = [ 'Mark', 12, 'goobers', 18.45 ] for it in items print it, " " end print "\n" # Go through the legal subscript values of an array. for i in (0...items.length) print items[0..i].join(" "), "\n" end The Ruby for loop is similar to the Python for or the perl foreach. Its basic operation is iterate over the contents of some collection. This may be an actual array or a range expression. A range expression is two numbers, surrounded by parens, and separated by either two dots or three. The three-dot form omits the last number from the range, while the two-dot version includes it. The three-dots version can be quite useful for creating a range of legal subscripts since the array size is not a legal subscript.

Case Expression

for i in (1..10) rno = rand(100) + 1 msg = case rno when 42: "The ultimate result." when 1..10: "Way too small." when 11..15,19,27: "Sorry, too small" when 80..99: "Way to large" when 100: print "TOPS\n" "Really way too large" else "Just wrong" end print "Result: ", rno, ": ", msg, "\n" end The ruby case statement is similar to the C/C++/Java switch, but more directly related to the similar (and superior) structures from Pascal and Ada. First, it assumes that each case ends where the next one starts, without needing a break to terminate a case. Secondly, each case can be expressed rather generally, with a single value, a range of value, or a list containing some of each. For this example, I'm using the fact the control statements have a return value. That's not special to cases, and their value can be used or not, just as ifs.

Iterators

# Here's a different way to add up an array. fred = [ 4, 19, 3, 7, 32 ] sum = 0 fred.each { |i| sum += i } print "Sum of [", fred.join(" "), "] is #{sum}\n" # Or create a secret message. key = { 'A' => 'U', 'B' => 'Q', 'C' => 'A', 'D' => 'F', 'E' => 'D', 'F' => 'K', 'G' => 'P', 'H' => 'W', 'I' => 'N', 'J' => 'L', 'K' => 'J', 'L' => 'M', 'M' => 'S', 'N' => 'V', 'O' => 'Y', 'P' => 'O', 'Q' => 'Z', 'R' => 'T', 'S' => 'E', 'T' => 'I', 'U' => 'X', 'V' => 'B', 'W' => 'G', 'X' => 'H', 'Y' => 'R', 'Z' => 'C' } print "\nThe encoded message is: " "The secret message".each_byte do | b | b = b.chr.upcase if key.has_key?(b) then print key[b] else print b end end print "\n" # But give us the info to read it anyway. print "The key is: " ct = 8 key.each { | k, v | if ct == 8 then print "\n " ct = 0 else print ", " end ct = ct + 1 print "#{v} => #{k}" } print "\n\n" # Some interesting things from Integer. 3.times { print "Hi! " } print "\n" print "Count: " 3.upto(7) { |n| print n, " " } print "\n"
See:Ruby Manual
Ruby iterators are methods which take and run a block of code. The block can be delimited by curly braces or by the keywords do and end. The brackets have higher precedence, and variables declared in them are destroyed when the bracked code exits. The parameter names for the block are listed between | symbols at the start of the block. This syntax is borrowed from Smalltalk. Note: Ruby uses the term "iterator" rather differently than C++.

Methods I

# Square the number def sqr(x) return x*x end # See how it works. (rand(4) + 2).times { a = rand(300) print a,"^2 = ", sqr(a), "\n" } print "\n" # Don't need a parm. def boom print "Boom!\n" end boom boom # Default parms print "\n" def line(cnt, ender = "+", fill = "-") print ender, fill * cnt, ender, "\n" end line(8) line(5,'*') line(11,'+','=') # Do they change? def incr(n) n = n + 1 end a = 5 incr(a) print a,"\n" Functions (and later methods) are defined with def. Parameters are given, without types of course. Default values may be provided. Note that when we send an integer, the method does not change the value sent.

Methods II

# Place the array in a random order. Floyd's alg. def shuffle(arr) for n in 0...arr.size targ = n + rand(arr.size - n) arr[n], arr[targ] = arr[targ], arr[n] if n != targ end end # Make strange declarations. def pairs(a, b) a << 'Insane' shuffle(b) b.each { |x| shuffle(a); a.each { |y| print y, " ", x, ".\n" } } end first = ['Strange', 'Fresh', 'Alarming'] pairs(first, ['lemonade', 'procedure', 'sounds', 'throughway']) print "\n", first.join(" "), "\n" Here's another use of methods. Notice that when we send an array as a parameter, changes in the function do update the value to the caller. One incidental thing we have not seen before is parallel assignment in the shuffle function which exchanges two values.

Methods III

# Add the strings before and after around each parm and print def surround(before, after, *items) items.each { |x| print before, x, after, "\n" } end surround('[', ']', 'this', 'that', 'the other') print "\n" surround('<', '>', 'Snakes', 'Turtles', 'Snails', 'Salamanders', 'Slugs', 'Newts') print "\n" def boffo(a, b, c, d) print "a = #{a} b = #{b}, c = #{c}, d = #{d}\n" end # Use * to adapt between arrays and arguments a1 = ['snack', 'fast', 'junk', 'pizza'] a2 = [4, 9] boffo(*a1) boffo(17, 3, *a2) The * is used to adapt between arrays and parameter or argument lists. In a call, a * before an array treats the array members as separate arguments. A * before the last parameter causes it to receive all remaining arguments as an array.

Exceptions I

# Count and report the number of lines and characters in a file. print "File name: " fn = gets.chomp begin f = open(fn) nlines = 0 length = 0 f.each { |line| nlines += 1; length += line.length } rescue print "File read failed: " + $! + "\n" else print fn, ": ", nlines, " lines, ", length, " characters.\n" end Ruby uses exceptions. Instead of try, the block is called begin, and instead of catch there is rescue. The else is executed when no exception occurs. There is also an ensure block which is always run last, exception or no.

Regular Expressions I

# Get a chomped string, or nil at EOF. def getstr print "Please enter a test string: " str = gets return str unless str return str.chomp end # Test strings while str = getstr print "You entered: ", str, "\n" # Run some random tests and print a descriptive message for ones which # match. num = 0 if str =~ /^\s*$/ then print " > Your string is all blanks.\n" next end if str =~ /Mommy/ then print " > Contains Mommy\n" num += 1 end if str =~ /Mommy.*Daddy/ then print " > Contains Mommy, then Daddy\n" num += 1 end if str !~ /CAT/ then print " > Does not contain CAT.\n" num += 1 end if str !~ /[Cc][Aa][Tt]/ then print " > Does not contain cat (any capitalization).\n" num += 1 end if str =~ /^AA/ then print " > Starts with AA\n" num += 1 end if str =~ /(ing|ed)$/ then print " > Ends in ing or ed\n" num += 1 end if str =~ /^\d+$/ then print " > Is an unsigned integer\n" num += 1 end if str =~ /^(\+|\-)\d+$/ then print " > Is a signed integer\n" num += 1 end if str !~ /[AEIOUaeiou]/ then print " > Contains no vowels.\n" num += 1 end if str =~ /@[^A-Z]*$/ then print " > Has an at sign with no upper case letters following it.\n" num += 1 end if str =~ /^[^%]*%[^%]*%[^%]*%[^%]*$/ then print " > Contains exactly 3 percent signs.\n" num += 1 end if str =~ %r=^(http|ftp)://([a-zA-Z-]+(\.[a-zA-Z-]+)+)(/|$)= then proto = $1 host = $2 print " > Looks like a lot of common URLs with protocol #{proto} ", "and host #{host}.\n" num += 1 end # What happened? if num == 0 then print "=== That string is remarkably boring. ===\n" else print "=== Found ", num, " interesting thing" + (if num > 1 then "s" else "" end), " about that string. ===\n" end end Regular expressions are patterns which can be matched against strings. The usual syntax is to surround them with slashes, which create a Regexp object, much as putting quoted characters create a String object. As the %Q for strings, the %r notation allows an alternate delimiter on regular expressions. In this program, use the =~ operator to ask if a string matches the pattern, and !~ to ask if it doesn't. Parentheses used in regular expressions group operations just as the do in ordinary expressions. In addition, the characters matched by each parenthetical group are assigned to a special variable, $n, where the left paren was the nth from the left. Regular expressions are a large topic, and we'll have some class discussion about them.

Regular Expressions II

# Using re's to break up a line. print "Please enter a line: " line = gets.chomp res = [ ] while res != '' # String leading blanks. line.sub!(/^\s*/, '') break if line == '' # See what the leading is for next action. if line[0].chr == '"' then # Quoted. line.sub!(/^"([^"]*)"/, '') res.push($1) elsif line.sub!(/^(\d+):(\S+)/, '') # Repeated with n: $1.to_i.times { res.push($2) } else # Just a word. line.sub!(/^(\S+)/, '') res.push($1) end end res.each { |x| print " [", x, "]\n" } The sub and sub! methods of String match their first argument (a regular expression), and replace the portion matched with their second (a string). This program breaks up the input line into words, with two special notations. Double-quoted sections are obeyed, which can contain spaces, and a word can be repeated by placing a count and a colon before it, like 5:fred instead of typing fred five times.

More List Ops

fred = [ 4, 9, 18, 3, 87, 9, 12 ] alex = [ 'Susan', 'Joe', 'Alex', 'Alice', 'Sam' ] # Compute a new array with each member of fred doubled. fred = fred.map { |x| 2 * x } print fred.join(" "), "\n" # Create a new alex adding " went away" to each member. Then join and # print the result. print (alex.map { |z| z + " went away" }).join(" "), "\n" # Print the members of fred which are more than five and less than 20. print (fred.select { |z| z > 5 & z < 20 }).join(" "), "\n" # Print the lengths of the members of alex that start with A or end with e. print ((alex.select { |n| n =~ /^A/ || n =~ /e$/ }).map { |z| z.length }). join(" "), "\n" # Update alex by surround each of its members with [ ] alex.map! { |a| "[" + a + "]" } print alex.join(" "), "\n" The built-in Array class contains a number of iterators which work on the entire list and return a list result. Two important ones are map, which runs the code block on each member of the list, and returns the list of results from those operations. The other is select, which also runs a code block on each member of the list. This block should return a boolean, and a new list is build of only those members for which the code block produces true. The code block can be thought of as a filter which decides, for each member, if it should be retained. The plain forms, map and select return a new array. The emphatic forms, map! and select! update the list.

Polynomial Evaluator

# # This program evaluates polynomials. It first asks for the coefficients # of a polynomial, which must be entered on one line, highest-order first. # It then requests values of x and will compute the value of the poly for # each x. It will repeatly ask for x values, unless you the user enters # a blank line. It that case, it will ask for another polynomial. If the # user types quit for either input, the program immediately exits. # # # Function to evaluate a polynomial at x. The polynomial is given # as a list of coefficients, from the greatest to the least. def polyval(x, coef) sum = 0 coef = coef.clone # Don't want to destroy the original while true sum += coef.shift # Add and remove the next coef break if coef.empty? # If no more, done entirely. sum *= x # This happens the right number of times. end return sum end # # Function to read a line containing a list of integers and return # them as an array of integers. If the string conversion fails, it # throws TypeError. If the input line is the word 'quit', then it # converts it to an end-of-file exception def readints(prompt) # Read a line print prompt line = readline.chomp raise EOFError.new if line == 'quit' # You can also use a real EOF. # Go through each item on the line, converting each one and adding it # to retval. retval = [ ] for str in line.split(/\s+/) if str =~ /^\-?\d+$/ retval.push(str.to_i) else raise TypeError.new end end return retval end # # Take a coeff and an exponent and return the string representation, ignoring # the sign of the coefficient. def term_to_str(coef, exp) ret = "" # Show coeff, unless it's 1 or at the right coef = coef.abs ret = coef.to_s unless coef == 1 & exp > 0 ret += "x" if exp > 0 # x if exponent not 0 ret += "^" + exp.to_s if exp > 1 # ^exponent, if > 1. return ret end # # Create a string of the polynomial in sort-of-readable form. def polystr(p) # Get the exponent of first coefficient, plus 1. exp = p.length # Assign exponents to each term, making pairs of coeff and exponent, # Then get rid of the zero terms. p = (p.map { |c| exp -= 1; [ c, exp ] }).select { |p| p[0] != 0 } # If there's nothing left, it's a zero return "0" if p.empty? # *** Now p is a non-empty list of [ coef, exponent ] pairs. *** # Convert the first term, preceded by a "-" if it's negative. result = (if p[0][0] < 0 then "-" else "" end) + term_to_str(*p[0]) # Convert the rest of the terms, in each case adding the appropriate # + or - separating them. for term in p[1...p.length] # Add the separator then the rep. of the term. result += (if term[0] < 0 then " - " else " + " end) + term_to_str(*term) end return result end # # Run until some kind of endfile. begin # Repeat until an exception or quit gets us out. while true # Read a poly until it works. An EOF will except out of the # program. print "\n" begin poly = readints("Enter a polynomial coefficients: ") rescue TypeError print "Try again.\n" retry end break if poly.empty? # Read and evaluate x values until the user types a blank line. # Again, an EOF will except out of the pgm. while true # Request an integer. print "Enter x value or blank line: " x = readline.chomp break if x == '' raise EOFError.new if x == 'quit' # If it looks bad, let's try again. if x !~ /^\-?\d+$/ print "That doesn't look like an integer. Please try again.\n" next end # Convert to an integer and print the result. x = x.to_i print "p(x) = ", polystr(poly), "\n" print "p(", x, ") = ", polyval(x, poly), "\n" end end rescue EOFError print "\n=== EOF ===\n" rescue Interrupt, SignalException print "\n=== Interrupted ===\n" else print "--- Bye ---\n" end This program reads and evaluates polynomials. The arithmetic uses integers, though it could be adapted easily enough to used floats outside the exponent values. Polynomials are read in as a list of coefficients. The number of values determines the order. The main program is a double loop, the outer reading polynomials, and the inner reading x values. The user can enter any number of polynomials, and, for each, evaluate them at any number of domain values. Either loop is ended by entering a blank line. An EOF will halt the program directly. A couple of novelties and notables: The polyval function takes an x value and a list of coefficients representing the polynomial and evaluates it at x. The evaluation loop is destructive, so the function begins by cloning the array so the original is un-damaged. The clone method, as you might imagine, makes a copy of the object. The built-in readline function is the same as gets, except it raises the end-of-file exception. Ruby gives you a choice to use exceptions or not. This program makes some use of the list operations. In particular, the polystr method which converts the polynomial to a string for printing uses map and select. The map converts the list of coefficients to a list of pairs, coefficient and exponent, then select eliminates the zero terms. When the user enters a bad polynomial, the program raises a TypeError. The reading loop catches this with a rescue, then issues a retry to allow the user to reenter the polynomial. The retry used in a rescue block simply starts the begin block over from the beginning.

Ruby Classes

# Class names must be capitalized. Technically, it's a constant. class Fred # The initialize method is the constructor. The @val is # an object value. def initialize(v) @val = v end # Set it and get it. def set(v) @val = v end def get return @val end end # Objects are created by the new method of the class object. a = Fred.new(10) b = Fred.new(22) print "A: ", a.get, " ", b.get,"\n"; b.set(34) print "B: ", a.get, " ", b.get,"\n"; # Ruby classes are always unfinished works. This does not # re-define Fred, it adds more stuff to it. class Fred def inc @val += 1 end end a.inc b.inc print "C: ", a.get, " ", b.get,"\n"; # Objects may have methods all to themselves. def b.dec @val -= 1 end begin b.dec a.dec rescue StandardError => msg print "Error: ", msg, "\n" end print "D: ", a.get, " ", b.get,"\n";

Box Class

# Box drawing class. class Box # Initialize to given size, and filled with spaces. def initialize(w,h) @wid = w @hgt = h @fill = ' ' end # Change the fill. def fill(f) @fill = f return self end # Rotate 90 degrees. def flip @wid, @hgt = @hgt, @wid return self end # Generate (print) the box def gen line('+', @wid - 2, '-') (@hgt - 2).times { line('|', @wid - 2, @fill) } line('+', @wid - 2, '-') end # For printing def to_s fill = @fill if fill == ' ' fill = '(spaces)' end return "Box " + @wid.to_s + "x" + @hgt.to_s + ", filled: " + fill end private # Print one line of the box. def line(ends, count, fill) print ends; count.times { print fill } print ends, "\n"; end end # Create some boxes. b1 = Box.new(10, 4) b2 = Box.new(5,12).fill('$') b3 = Box.new(3,3).fill('@') print "b1 = ", b1, "\nb2 = ", b2, "\nb3 = ", b3, "\n\n" # Print some boxes. print "b1:\n"; b1.gen print "\nb2:\n"; b2.gen print "\nb3:\n"; b3.gen print "\nb2 flipped and filled with #:\n"; b2.fill('#').flip.gen print "\nb2 = ", b2, "\n"

Inheritance

# Class names must be capitalized. Technically, it's a constant. class Fred # The initialize method is the constructor. The @val is # an object value. def initialize(v) @val = v end # Set it and get it. def set(v) @val = v end def get return @val end def more(y) @val += y end def less(y) @val -= y end def to_s return "Fred(val=" + @val.to_s + ")" end end # Class Barney is derived from Fred with the usual meaning. class Barney < Fred def initialize(x) super(x) @save = x end def chk @save = @val end def restore @val = @save end def to_s return "(Backed-up) " + super + " [backup value: " + @save.to_s + "]" end end # Objects are created by the new method of the class object. a = Fred.new(398) b = Barney.new(112) a.more(34) b.more(817) print "A: a = ", a, "\n b = ", b, "\n"; a.more(34) b.more(817) print "B: a = ", a, "\n b = ", b, "\n"; b.chk a.more(34) b.more(817) print "C: a = ", a, "\n b = ", b, "\n"; b.restore print "D: a = ", a, "\n b = ", b, "\n";

Setting Variables

# Class names must be capitalized. Technically, it's a constant. class Fred # The initialize method is the constructor. The @val is # an object value. def initialize(v) @val = v end # Set it and get it. def set(v) @val = v end def to_s return "Fred(val=" + @val.to_s + ")" end # Since a simple access function is so common, ruby lets you declare one # automatically, like this: attr_reader :val # You can list any number of object variables. Separate by commas, and each # needs its own colon # attr_reader :fred, :joe, :alex, :sally end class Alice <Fred # We have a message, too. def initialize(n, m) super(n) @msg = m end # Takes the base result and changes the class name. def to_s ret = super ret.gsub!(/Fred/, 'Alice') return ret + ' ' + @msg + '!' end # The = allows the method to be used on the right, and the left of the # assignment is the parameter. def appmsg=(more) @msg += more end # Like attr_reader, if you want the data to be assignable. attr_writer :msg end a = Fred.new(45) b = Alice.new(11, "So there") print "A: a = ", a, "\n b = ", b, "\n" print "B: ", a.val, " ", b.val, "\n" b.msg = "Never" print "B: b = ", b, "\n" b.appmsg = " In a million years" print "C: b = ", b, "\n"

Gate Classes

# # Ruby circuit simulation classes. This file contains a base class Gate, # and several derived classes describing digital logic gates. There are # also classes for input and display. There's also a flip-flop. # class Gate # This is a count of the "active" gates, which are ones which have received # a signal but not resolved it. @@active = 0 # This is a list of gates which have registered that they want to be # notified when the circuit is quiet. They give an integer priority, # and are notified in increasing priority order. @@needquiet = { } def quiet_register(pri) if ! @@needquiet.key?(pri) then @@needquiet[pri] = [ ] end @@needquiet[pri].push(self) end # Here's how we set stuff. There are static and object versions of # each, since I may want to activate from other spots. def Gate.activate @@active += 1 end def activate Gate.activate end def Gate.deactivate @@active -= 1 if @@active == 0 then @@needquiet.keys.sort.each \ { |p| @@needquiet[p].each { |g| g.onquiet } } end end def deactivate Gate.deactivate end # This is the default quiet action (nothing). def onquiet end # A signal is directed to a particular port on a particular gate. This # encapsulates those two data. When a gate connects to us, we send back # one of these to direct its later signal changes. class LinkHandle def initialize(sink_gate, sink_port) @sinkg = sink_gate @sinkp = sink_port end # The sending gate uses this method to forward the signal to the # downstream gate. def signal(value) @sinkg.signal(@sinkp,value) end attr_reader :sinkg, :sinkp end def initialize(ival = false) @inputs = [ ] # Array of inputs (boolean values) @outputs = [ ] # Array of LinkHandle objects where to send output @outval = ival # Present output value. end # This is called when a input gate sends us a signal on a particular input. # We recompute our output value, and, if it changes, we send it on to all # of our outbound connections. def signal(port, val) # The derived class needs to implement the value method. self.activate @inputs[port] = val newval = self.value if newval != @outval then @outval = newval @outputs.each { | c | c.signal(newval) } end self.deactivate end # Call this to connect your output to the next one of our inputs. def connect(v) port = @inputs.length @inputs.push(v) c = LinkHandle.new(self, port) self.signal(port, v) return c end # Join me to another gate. def join(g) @outputs.push(g.connect(@outval)) end def joinmany(*p) p.each { |i| self.join(i); } end attr_reader :outval # Some printing help def name return self.class.to_s end def insstr return (if @inputs.length == 0 then "-" else @inputs.join('.') end) end def to_s return name + " " + insstr + " => " + @outval.to_s end # Create another object of the same type. def another return self.class.new end def manyothers(n) ret = [] n.times { ret.push(self.another) } return ret end # This manufactures any number of objects. It is a static method, and # inherited by the real gates. The expression self.new, then, runs the # new method on the actual object, which the inheriting class. Therefore, # it will create any gate. def Gate.many(n) ret = [ ] n.times { ret.push(self.new) } return ret end # Dump a whole circuit. Yecch. def outlinks return @outputs end def Gate.dump(*roots) ct = -1 gatemap = { } for g in roots gatemap[g] = (ct += 1) unless gatemap.has_key?(g) end printed = { } while roots.length > 0 g = roots.shift next if printed.has_key?(g) print "[", gatemap[g], "] ", g, ":" for c in g.outlinks og = c.sinkg gatemap[og] = (ct += 1) unless gatemap.has_key?(og) print " ", gatemap[og], "@", c.sinkp roots.push(og) end print " [none]" if g.outlinks.length <= 0 print "\n" printed[g] = true end end end # Standard and gate class AndGate < Gate def initialize super(true) end def value for i in @inputs return false if !i end return true end end class NandGate < AndGate def value return ! super end end # Standard or gate class OrGate < Gate def value for i in @inputs return true if i end return false end end class NorGate < OrGate def value return ! super end end # Standard xor gate class XorGate < Gate def value ret = false for i in @inputs ret ^= i end return ret end end # Gates with a limited number of input connections. class LimitedGate < Gate def initialize(max=1,i=false) super(i) @max = max end # Enforce connect limit. def connect(v) if @inputs.length >= @max then raise TypeError.new("Too many input connections.") end super(v) end end # Not gate. class NotGate < LimitedGate def initialize super(1,true) end def value return ! @inputs[0] end end # This is a "yes gate" or amplifier. It just forwards its input to all its # outputs class Connector < LimitedGate def value return @inputs[0] end # We can also use it as a one-bit input device. def send(v) self.signal(0,v) end end # D Flip-Flop. Level-triggered. First input is D, second is clock. class FlipFlop < LimitedGate def initialize super(2) end def value return (if @inputs[1] then @inputs[0] else @outval end) end end # D Flip-Flop. Edge-triggered. First input is D, second is clock. # I think the level-triggered might make a lot more sense with this # simulation, though these are better in circuits. class FlipFlopET < FlipFlop def initialize super @newval = false end def value return @newval end def signal(port, val) # Need to stick our fingers in this thing to find the rising edge. self.activate @newval = if port == 1 & !@inputs[1] & val then @inputs[0] else @outval end super(port,val) self.deactivate end end # Simple test point class Tester < LimitedGate def initialize(name="Tester") super(1) @name = name end attr_writer :name def value print @name, ": ", if @inputs[0] then "on" else "off" end, "\n"; return @inputs[0] end end # Numeric output device. Connect lines starting with LSB. class NumberOut < Gate @@quiet = false def NumberOut.shush(q=true) @@quiet = q end def initialize(name="Value", pri = 1) @name = name quiet_register(pri) super() end attr_writer :name # Print the value on quiet. def onquiet return if @@quiet; val = 0 @inputs.reverse_each { |i| val <<= 1 if i then val |= 1 end } print @name, ": ", val, "\n" end def value return false end end # LED which prints when circuit becomes quiet. class LED < NumberOut def initialize(name="LED", pri = 1) super(name, pri) end def onquiet if @inputs.length > 0 & ! @@quiet then print @name, ": ", if @inputs[0] then "on" else "off" end, "\n" end end def connect(v) if @inputs.length >= 1 then raise TypeError.new("Too many input connections.") end super(v) end end # Base for input devices. Mostly deals will collecting connections. class InputDevice def initialize @targs = [] end # Add a connection def join(g) @targs.push(g.connect(false)) end def joinmany(*p) p.each { |i| self.join(i); } end def outlinks return @targs end end # Switch bank. Connects to any number of gates, and will feed them a # binary number (as a string). Connections start with LSB. Initially, # all the switches are off. class SwitchBank < InputDevice # Send a number. Can take an integer or a string. def set(n) if n.is_a?(TrueClass) || n.is_a?(FalseClass) then @targs.each { | x | x.signal(n) } elsif n.is_a?(Integer) then @targs.each { | x | x.signal(n&1 == 1); n >>= 1 } else # Assume n is an ascii string of 1's and 0's. if n.length < @targs.length then n = ('0' * (@targs.length - n.length)) + n end sub = n.length - 1 @targs.each { | x | x.signal(n[sub].chr != "0"); sub -= 1 } end end # This is like switch, but it keeps the circuit active during each # sending. def value=(n) Gate.activate self.set(n) Gate.deactivate end end # Send a pulse (clock tick?) class Pulser < InputDevice def pulse Gate.activate @targs.each { |t| t.signal(true); } @targs.each { |t| t.signal(false); } Gate.deactivate end end

Circuit Test I

require("csim") S = SwitchBank.new A = AndGate.new #A.join(Tester.new("A")) B = OrGate.new #B.join(Tester.new("B")) C = XorGate.new #C.join(Tester.new("C")) L = LED.new('Result'); S.join(A) S.join(A) A.join(B) S.join(B) B.join(C) S.join(C) C.join(L) for x in (0..15) for s in [3,2,1,0] print (x>>s) & 1 end print " => " S.value = x end

Circuit Test II

require("csim") S = SwitchBank.new A = AndGate.new #A.join(Tester.new("A")) B = OrGate.new #B.join(Tester.new("B")) C = AndGate.new #C.join(Tester.new("C")) D = OrGate.new #D.join(Tester.new("D")) E = NotGate.new L = LED.new('Result') A.join(D) B.join(D) C.join(D) D.join(E) D.join(A) D.join(C) S.join(A) S.join(B) S.join(B) S.join(C) E.join(L) for x in (0..15) for s in [3,2,1,0] print (x>>s) & 1 end print " => " S.value = x end

Circuit Test III

require("csim") # The circuit here should simulate a level-triggered D flip-flop D = SwitchBank.new C = Pulser.new N = NotGate.new A1, A2 = AndGate.many(2) NR1, NR2 = NorGate.many(2) Q = LED.new(" Q") Qnot = LED.new(" ~Q") D.join(N) N.join(A1) D.join(A2) C.join(A1) C.join(A2) A1.join(NR1) A2.join(NR2) NR1.join(NR2) NR2.join(NR1) LED.shush(true) NR1.join(Q) NR2.join(Qnot) LED.shush(false) vals = [ true, false, false, true, false ] 10.times do 3.times do dval = vals.shift vals.push(dval) print "Input: ", dval, "\n" D.value = dval end print "=== TIC ===\n" C.pulse end

Subassemblies

require "csim" # This represents a compound of gates. It has an interface similar enough # to Gate to be used in place of one, though it isn't derived from Gate. # Ruby thinks this is just fine. # To build compound circuits, you must create a Blueprint. To do so: # 1. Create the Blueprint object. # 2. Create the objects, and connect them to each other. Don't make # any external connections. # 3. As needed specify gates you create as input or output devices for # the compund. # 4. Call lock(). This completes the Bluprint and makes it ready to # generate compounds. # The resulting Blueprint is also a Compound. It can be connected and used # like a Gate (though it is not derived from Gate). The another method # of Blueprint creates a Compound, which can be used in a circuit, but # lacks the building infrastructure. The Compound remembers its Blueprint, # and its another method simply calls the one in Blueprint. # # You should not add the same component to multiple compounds. class Compound protected # This is called only by Blueprint. Clients don't get to create # Compounds themselves. def initialize(ingates, outgates, blueprint) @ingates = ingates # Initially array of input gates, replaced # on each connect with a connection to it. @conndex = -1 # Index for connecting to it. @outgates = outgates # Array of output gates. @joindex = -1 # Index for joining. @blueprint = blueprint # Our blueprint object. end public # Connect to a specific input port. def portconn(port, v) c = @ingates[port] c.signal(v) return Gate::LinkHandle.new(self,port) end # Connect to the next input port. def connect(v) return portconn(@conndex += 1, v) end # Join an output to another device. Outputs are joined in the order # specified by outputs, or out(n) may be used to join to a specific output. def out(n) return @outgates[n] end def join(gate) @outgates[@joindex += 1].join(gate) end # Handle inbound signals. def signal(port, val) Gate.activate @ingates[port].signal(val) Gate.deactivate end # All the links on the output list, except for internal connections. def outlinks ret = [ ] for i in (0...@outgates.length) outs = @outgates[i].outlinks for j in (0...outs.length) ret.push(outs[j]) if outs[j] & @blueprint.internmap[i][j] != 1 end end return ret end # This creates another one of us. This runs in the blueprint, which # has more information. def another return @blueprint.another end # Create several others of us in an array. def manyothers(n) ret = [] n.times { ret.push(self.another) } return ret end # For prettier printing. def to_s ret = self.class.to_s st = @blueprint.subtype ret += " [" + st + "]" if st return ret + " " + self.object_id.to_s end end # Class Blueprint is used to describe a component made of other components. class Blueprint < Compound # Note: Initialization is not really complete until the lock method is # called. def initialize(subtype = nil) super([], [], self) @allgates = [ ] # This is a list of connections to internal gates # which represent the device inputs. @allcons = [ ] # List of [src, sink] in proper connect order. @internmap = [ ] # For each output gate, a mask of connections which # are internal. @subtype = subtype end # Specify gates as inputs to the circuits. You may specify any number of # Gates or Compounds, or arrays thereof. Each input makes a connection to # the specified gate in the order given; gates may be repeated when the # they form more than one input. If input order matters, be careful to # mix the specification of gates as inputs, and the joining of internal # connections, in the correct order. def inputs(*gates) for g in flatten(gates) @ingates.push(g.connect(false)) end end # This specifies some gates which are outputs. Gates, Compounds, or arrays # thereof may be specified. Output connections are made to these gates # in the order given. Gates may be repeated to supply multiple ouputs. # It is also possible to use a specific output connection number to # join multiple devices to the same port. def outputs(*gates) @outgates += flatten(gates) end # This closes the definition. It finds all the objects in the collection, # and makes a list of pairs that can be used to reproduce all the connections # preserving relative order at each end. This involves a topological sort. # Yecccch. def lock # Create the initial pending list of all input gates, plus all the # output gates (which should be redundant, but ...) gset = { } ((@ingates.map { |c| c.sinkg }) + @outgates).each { |g| gset[g] = true } pend = gset.keys # Process pending gates for outlinks. Find all devices which are # connected downstream from an input or output. while g = pend.shift # Scan the receiving gates. We take the list of outbound connections # and extract the sink gates therein, and run through that. for t in g.outlinks.map { |lnk| lnk.sinkg } if ! gset.has_key?(t) then # If not already in the set, add it to the set and the pending list. pend.push(t) gset[t] = true end end end # These are all the reachable gates; all the gates in the device. @allgates = gset.keys # Allocate graph nodes for each connection. This allocates a node for # each connection between two contained gates. Each node is an array # of the form # [ srcnext, sinknext, predcnt, src, sink ] # Where the first two point to nodes that come later in the order at # the source or destination (respectively), predcnt is an integer number # of nodes which must come before this one, and src and sink are the # gates at the start and end of the connection. This loop allocates # such nodes for each connection, adds links (only) for the source # ordering, and places them in a hash so we can find them by destination. nmap = { } n = 0 for g in @allgates lnode = nil # Previous node in source ordering. for c in g.outlinks # Create the node and store it in the hash. key = [ c.sinkg, c.sinkp ] node = [ nil, nil, (if lnode then 1 else 0 end), g, c.sinkg ] nmap[key] = node # Add a link here to the previous node. lnode[0] = node if lnode lnode = node end n += 1 end # This adds more nodes to represent the array of input links. These # nodes are like the above, except source position is nil. lnode = nil for c in @ingates do key = [ c.sinkg, c.sinkp ] node = [ nil, nil, (if lnode then 1 else 0 end), nil, c.sinkg ] nmap[key] = node lnode[0] = node if lnode lnode = node end # Now we go through and add links representing the ordering at the # destination. for k in nmap.keys # Scan all node keys. gate, port = k # Get the destination information targ = [gate, port+1] # Get key of next node in destination order if nmap.has_key?(targ) # See if such a node exists. tnode = nmap[targ] # Get the sink successor from the hash. tnode[2] += 1 # Increment its pred. count for source node. nmap[k][1] = tnode # Add a link from source to sink node. end end # Find all the roots (nodes without predecessors). This is the initial # value of links which may be established, since all links which must # appear earlier have been created. ready = nmap.values.select { |n| n[2] == 0 } # Traverse the graph obeyong all the constratints and produce a list # of connections in an order which will preserve the order at each end. # That preserves the creation order of the links in case it matters. @allcons = [ ] while n = ready.shift # Extract the contents of the ready node and add the relevant information # to the output order list. srcnext, sinknext, count, source, sink, sinkp = n @allcons.push([source, sink]) # Reduce the count of each successor, and add it each to the ready list # if it has no more predecessors. if srcnext then srcnext[2] -= 1; ready.push(srcnext) if srcnext[2] == 0 end if sinknext then sinknext[2] -= 1; ready.push(sinknext) if sinknext[2] == 0 end end # This makes a record of all internal links outbound from output # devices. These are not to be reported as connections out from the # compound. They are recorded in an array parallel to @outgates. # For each output gate, they contain a bitmap showing ones for each # position occupied at this time. @internmap = @outgates.map do |g| ret = 0 bit = 1 g.outlinks.each { |c| if c then ret &= bit end; bit <<= 1; } ret end # Whew! end # Make a Compound like this one. def another # Make copies of all the objects, and keep a map from the original to the # copy, so we can copy the connections. copymap = { } @allgates.each { |g| copymap[g] = g.another } # Reproduce all the connections on the new gates. Use the order computed # by close which preserves order of connections at each end. ingates = [ ] for c in @allcons if c[0] then copymap[c[0]].join(copymap[c[1]]) else # nil source indicates an input connection ingates.push(copymap[c[1]].connect(false)) end end # Construct the new compound using the new gates. return Compound.new(ingates, @outgates.map { |g| copymap[g] }, self) end attr_reader :internmap, :subtype private # This flattens an list by opening component arrays. def flatten(a) ret = [] a.each { |x| if x.is_a?(Array) then ret += x else ret.push(x) end } return ret end end

One-Bit Adder Subassembly

# # This defines a one-bit adder. # require "csim" require "cgrp" class OBA < Blueprint def initialize super("one-bit adder") # Get the parts. a,b,cin = Connector.many(3) gA = XorGate.new gB,gC,gD = AndGate.many(3) gE = OrGate.new # Hook 'em up. a.joinmany(gA,gB,gC) b.joinmany(gA,gB,gD) cin.joinmany(gA,gC,gD) [gB,gC,gD].each { |g| g.join(gE) } # Put it into a box. self.inputs(a,b,cin) self.outputs(gA, gE) self.lock end end

Eight-BIt Adder

# # This is a one-bit adder. # require "csim" require "cgrp" require "oba" NumberOut.shush # Blueprint for a the one-bit adder bp = OBA.new # Two input senders, and the output device. na = SwitchBank.new nb = SwitchBank.new disp = NumberOut.new(" Sum") # We're going to build an 8-bit adder prev = nil 8.times do # Create the one-bit adder and join the data inputs and outputs. addr = bp.another na.join(addr) nb.join(addr) addr.join(disp) # Chain the carry, if this isn't he first one. if prev then prev.join(addr) end prev = addr end # Overflow light. prev.join(LED.new(" Oflow")) NumberOut.shush(false) 30.times do a = rand(256) b = rand(256) print a, " + ", b, ":\n" Gate.activate na.value = a nb.value = b Gate.deactivate end

N-Bit Adder Subassembly

# # An N-bit adder built of one-bit adders with simple carry propagation. # require "oba" class Adder < Blueprint # This initializes the object, If we already have a one-bit adder # blueprint lying around, we can reuse it with that second argument. def initialize(n, bp = nil) super(n.to_s + "-bit adder") # Blueprint for a the one-bit adder bp = OBA.new unless bp @one_bit_bp = bp # Make all the adders, and specify them as inputs. addrs = bp.manyothers(n) self.inputs(addrs,addrs) # Create output connectors and chain the carries. prev = nil for addr in addrs c = Connector.new addr.join(c) if prev then prev.out(1).join(addr) end self.outputs(c) prev = addr end # Overflow self.outputs(prev) self.lock end attr_reader :one_bit_bp end

Adder Test

# # This tests the adder simulation by building various size adders and running # them various numbers of times. # require "csim" require "adder" # Size of the adder. size = 8 rpt = 20 size = $*[0].to_i if $*.length > 0 rpt = $*[1].to_i if $*.length > 1 NumberOut.shush # Blueprint for the 10-bit adder. bp = Adder.new(size) addr = bp.another # Hook up two input switch banks, one output, and an overflow led. na = SwitchBank.new size.times { na.join(addr) } nb = SwitchBank.new size.times { nb.join(addr) } disp = NumberOut.new(" Sum") size.times { addr.join(disp) } addr.join(LED.new(" Oflow")) #Gate.dump(na) NumberOut.shush(false) # Perform 30 addtions of random numbers. For each, we choose two random # inputs, and set them into the input switch banks. The outputs will print, # being the sum numeric display and the overflow LED. print "== Performing #{rpt} tests on #{size}-bit adder. ", "Max sum is #{(1<<size)-1} ==\n" rpt.times do # Choose randoms and print them. a = rand(1<<size) b = rand(1<<size) print a, " + ", b, ":\n" # Add them, and keep the circuits active to suppress printout until after # the whole operation is complete. Gate.activate na.value = a nb.value = b Gate.deactivate end

Unit Conversion

This is a lot of code to demonstrate something simple. The module is a convenient place to group defintions which work together. In this case, to perform unit conversions. The next file is a short driver which uses this module. # # This module provides unit conversion. The main public name is # the method ratio, which gives the ratio of two dimensioned expressions, # or throws an exception if they do not conform. The exceptions are also # public, as are the BaseUnit and RelatedUnit classes, which can be used # to add conversion information to the module. # # Units.ratio(from, to) # Will tell you how many to you need to equal from. From and to are strings # of the form # [ n ] { [ / ] unitname[ ^r ] }+ # Where n is the number of whatever units, defaulting to 1.0. The unitname # is a unit or alias created with BaseUnit or RelatedUnit. Unitnames may # be separated by spaces or dashes. Power on the unit defaults to 1. module Units public # Two new exceptions just for us to pitch. class UnitsException < Exception # Base class for exceptions invented for this module. end # A unit expression string couldn't be parsed. class UnitParseError < UnitsException def initialize(m = "Cannot parse measurement expression") super(m) end end # Conversion of units which are not conformable (feet to liters or like # that). class UnitConformability < UnitsException def initialize(m = "Units not conformable") super(m) end end # Abstract base class for units. class Unit # A list of all the units we know. @@units = { } # Access to @@units. def Unit.exists(n) return @@units.has_key?(n) end def Unit.named(n) return @@units[n] end # If this were Java, I'd define an abstract function isbase() which tells # if this object is a BaseUnit or not. def initialize(name) @name = name @@units[name] = self @@units[name + 's'] = self end attr_reader :name def alias(*names) names.each { |n| @@units[n] = self } end end # This is the base unit class. There is one base unit for each # dimension. Any unit in the right dimension could be used. # The class is just a name and a dimension name. class BaseUnit < Unit def isbase() return true end def initialize(name, dname) super(name) @dimension = dname end attr_reader :dimension # This orders the unit objects arbitrarily, but that is sufficient to # sort them and compare lists. def <=> (u) return object_id <=> u.object_id end end private # Here are the base units for each dimension. BaseUnit.new("meter", "length").alias("m", "metre") BaseUnit.new("gram", "mass").alias("g") BaseUnit.new("second", "time").alias("s", "sec") # A measurement is a number and some numerator and denominator units. # The unit lists are kept in lowest terms of base units, though the object # may be initialized with any units. class Measurement def initialize(m, num, denom) @mult = m.to_f # The multiplier. to_f in case you send an integer. @num = num # The numerator units @denom = denom # The denominator units. normalize # Convert to lowest terms of base units. end attr_reader :mult, :num, :denom # Return the ratio. Throws conformability. def ratio(divby) raise UnitConformability.new \ if @num != divby.num || @denom != divby.denom return @mult / divby.mult end private # Convert to base units only, in lowest terms. def normalize # Convert the lists to just base units. newnum = [] newdenom = [] basify(@num, false, newnum, newdenom) basify(@denom, true, newdenom, newnum) # Now eliminate units which appear in both places. This depends on # an arbitrary ordering of the base units which allows us to compare # them in a merge order. newnum.sort! newdenom.sort! @num = [ ] @denom = [ ] while newnum.length > 0 & newdenom.length > 0 rel = newnum[0] <=> newdenom[0] if rel < 0 # They are different and the num comes first. Must be kept. @num.push(newnum.shift) elsif rel > 0 # They are different and the denom comes first. Must be kept. @denom.push(newdenom.shift) else # A match. Eliminate. newnum.shift newdenom.shift end end @num.concat(newnum) @denom.concat(newdenom) end # Convert the source list to base, adding the components to ndest and # ddest, and multiplying or dividing the measure's number. The # src is a unit list. These may not be base units, but, if not, the # measures they contain will be. def basify(src, divide, ndest, ddest) for unit in src if unit.isbase ndest.push(unit) else @mult = if divide then @mult / unit.related.mult else @mult * unit.related.mult end ndest.concat(unit.related.num) ddest.concat(unit.related.denom) end end end end # This can be called as (qty, unitex) or just (unitex), where m is # taken as 1.0. Units expression is name[^pwr] [ / name... ] # unit names are always alpha. - is a separator like a space, but not # between ^ and pwr. def Units.qty(m, s=nil) m,s = 1.0,m if s == nil # See if there's a number at the front of the expression. if s.sub!(/^\s*(\-?(\d+(\.\d*)?|\d*\.\d+))\s*/, '') m *= $1.to_f end # Collect the stuff needed for the units part. num = [] denom = [] # What's the next thing? puthere = num otherone = denom while true # Strip leading crud, which is spaces or dashes s.sub!(/^(\s|\-)*/, '') break if s == '' # Find the next "thing", which is a unit name or a /. s.sub!(%r=^([a-zA-Z]+|/)=, '') or raise UnitParseError.new('Expected unit name or slash at "' + s + '"' ) thing = $1 if thing == '/' # Swap which list the units go into. puthere, otherone = otherone, puthere else # Unit name. Unit.exists(thing) or raise UnitParseError.new('Unknown unit name "' + thing + '"') unit = Unit.named(thing) # See if there's a ^n ct = 1 top = true if s.sub!(/^\s*\^\s*(\-?)(\d+)/, '') ct = $2.to_i top = false if $1 != '' end if top ct.times { puthere.push(unit) } else ct.times { otherone.push(unit) } end end end return Measurement.new(m, num, denom) end public # Return the ratio. def Units.ratio(top, bot) return Units.qty(top).ratio(Units.qty(bot)) end # The units that are not basic class RelatedUnit < Unit def isbase() return false end def initialize(name, measure) super(name) measure = Units.qty(measure) if measure.kind_of?(String) @related = measure end attr_reader :related end private # Here are all the rest of the units we know about. RelatedUnit.new("kilometer", "1000 meter").alias("km") RelatedUnit.new("centimeter", "0.01 meter").alias("cm") RelatedUnit.new("milimeter", "0.01 meter").alias("mm") RelatedUnit.new("inch", "2.54 cm").alias("in") RelatedUnit.new("foot", "12 in").alias("ft", "feet") RelatedUnit.new("mile", "5280 ft").alias("mi") RelatedUnit.new("yard", "3 ft").alias("yd") RelatedUnit.new("furlong", "660 ft") RelatedUnit.new("milliliter", "cm^3").alias("ml", "cc") RelatedUnit.new("liter", "1000 ml").alias("l") RelatedUnit.new("gallon", "3.785412 liter").alias("gal") RelatedUnit.new("quart", "0.25 gal").alias("qt") RelatedUnit.new("pint", "0.5 quart").alias("pt") RelatedUnit.new("cup", "0.25 quart") RelatedUnit.new("acre", "43560 ft^2") RelatedUnit.new("hectare", "10000 m^2") RelatedUnit.new("minute", "60 sec").alias("min") RelatedUnit.new("hour", "60 min").alias("hr") RelatedUnit.new("day", "24 hr") RelatedUnit.new("week", "7 day").alias("wk") RelatedUnit.new("fortnight", "14 day") RelatedUnit.new("year", "365.25 day").alias("yr") RelatedUnit.new("kilogram", "1000 gram").alias("kg") RelatedUnit.new("slug", "14.593903 kg") RelatedUnit.new("newton", "kg-m/s^2").alias("N") RelatedUnit.new("pound", "4.448222 N").alias("lb") RelatedUnit.new("joule", "N-m").alias("J") RelatedUnit.new("calorie", "0.238846 J").alias("cal") RelatedUnit.new("kcal", "1000 cal") RelatedUnit.new("BTU", "1055.055853 J") RelatedUnit.new("watt", "J/s") RelatedUnit.new("kilowatt", "1000 watt").alias("kw") RelatedUnit.new("horsepower", "746 watt") RelatedUnit.new("knot", "1.68781 ft/sec") end

Unit Conversion Driver

# # This is a small program which uses the Units module to perform conversions # requested by the user. require("units") print "You have: " while from = gets from.chomp! print "You want: " to = gets.chomp begin result = Units.ratio(from.clone, to.clone) print from, " = ", result, " ", to, "\n" rescue Exception # Stupidly enough, Exception isn't the default; StandardError is, which is # a subclass of Exception. print "Failed: #{$!}\n" end print "You have: " end

Inclusion Modules

# The two small modules here are intended to contain generic facilities # which can be used by classes. # This follows using the next method until we get to the end of whatever it # is. module Follower def last at = self while true n = at.next if n == nil then return at end at = n end end end # This prints on one line using the each method. module Printer def pr(newline = false) self.each { |x| print x, " " } print "\n" if newline end end Modules can be used to provide generic facilities using the include facility. The methods in the modules (well, one each in this case) can be added to classes using the include keyword. See the next page for its use.

Linked List

require("last") # Here is a linked list class. Since there's not much point in writing # such a class when you already have all the Ruby data structures # available, you might have figured out it's here to demonstrate something: # including a module. # # A linked list class List # Nodes for the linked list. class Node # Get the last facility which scans to the end of the list. include Follower def initialize(d, n = nil) @val = d @next = n end attr_reader :next, :val attr_writer :next end # Get the printing facility. include Printer # Create the list with its first node. def initialize(first) @head = Node.new(first) end # Add at the front. We can only add, and the list is created with one # node, so no special case for empty list. How nice. def at_front(v) n = Node.new(v) n.next = @head @head = n end # Add to the end of the list. def at_end(v) n = Node.new(v) @head.last.next = n end # Process each member of the list. The yield operator calls the block # sent to the function. def each p = @head while p != nil yield p.val p = p.next end end end
See:Programming Ruby
The List class, and Node class inside it, use the include directive to add methods from the Follower and Printer modules, respectively. Note the implementation of the iterator for List: the yield operation runs the code block provided to the iterator.

Tree

require("last") # A BST class. Again, the purpose is a demo, not because you need one when # you already have the Ruby datatypes. class Tree class Node # Again, we include the follower class. include Follower def initialize(d) @val = d @lft, @rgt = nil end attr_reader :lft, :rgt, :val attr_writer :lft, :rgt # Our next function just moves right. (See below) def next return @rgt end # Insert a new node into the subtree rooted here. def insert(new) if new.val < @val then if @lft == nil then @lft = new else @lft.insert(new) end else if @rgt == nil then @rgt = new else @rgt.insert(new) end end end # This runs for each value in the tree in sorted order. The block # parameter is an object known as a closure. They contain executable # code, and blocks can become closures (see below). def each(block) if @lft then @lft.each(block) end block.call(@val) if @rgt then @rgt.each(block) end end end # Get the printing facility. include Printer def initialize(first) @root = Node.new(first) end # Insert a value. Most of the work in Node#insert def insert(v) @root.insert(Node.new(v)) end # Stepping right from the root until nil gives the max value in the tree. def max return @root.last.val end # The &blk notation converts the block used in the iterator into a # closure object, which we send to Node#each. def each(&blk) @root.each(blk) end end
See:Programming Ruby
The Tree class also includes the common operations. This one also implements an each iterator. This one uses the parameter &blk. The & converts the code block to a Closure object. Closures are executable code. It is bound to the parameter blk which is sent to Node#each. There it becomes a parameter block, and is run with the call method.

Driver

require("list") require("tree") print "=== List test ===\n" x = List.new(10) x.at_front(33) x.at_front(28) x.at_end(12) x.at_front(3) x.at_end(71) x.pr(true) s = 0 x.each { |n| s += n } print "sum = ", s, "\n" print "\n=== Tree test ===\n" t = Tree.new(28) t.insert(38) t.insert(1) t.insert(39) t.insert(17) t.insert(22) t.insert(8) t.insert(11) t.pr(true) s = 0 t.each { |n| s += n } print "sum = ", s, "\n" print "Max is ", t.max, "\n"

Button

#!/usr/bin/ruby # Import the library. require 'tk' # Root window. root = TkRoot.new { title 'Push Me' } # Add a label to the root window. lab = TkLabel.new(root) { text "Push the Button" } # Make it appear. lab.pack # Here's a button. Also added to root by default. TkButton.new { text "PUSH" command { print "Arrrrrrg!\n" } pack } Tk.mainloop

Colors

#!/usr/bin/ruby # Import the library. require 'tk' # Root window. root = TkRoot.new { title 'Push Me' background '#111188' } # Add a label to the root window. lab = TkLabel.new(root) { text "Push the Button" background '#3333AA' foreground '#CCCCFF' } # Make it appear. lab.pack('side' => 'left', 'fill' => 'y') # Here's a button. Also added to root by default. TkButton.new { text "PUSH" background '#EECCCC' activebackground '#FFEEEE' foreground '#990000' command { print "Arrrrrrg!\n" } pack('side' => 'right') } Tk.mainloop

Configure

#!/usr/bin/ruby # Import the library. require 'tk' # Root window. root = TkRoot.new { title 'Push Me' background '#111188' } # Add a label to the root window. lab = TkLabel.new(root) { text "Hey there,\nPush a button!" background '#3333AA' foreground '#CCCCFF' } # Make it appear. lab.pack('side' => 'left', 'fill' => 'both') # A frame can be used to arrange buttons with the packer. fr = TkFrame.new fr.pack('side' => 'right', 'fill' => 'both') # Here's a button. Added to the frame, not the root. swapbut = TkButton.new(fr) { text "Swap" background '#EECCCC' activebackground '#FFEEEE' foreground '#990000' pack('side' => 'top', 'fill' => 'both') } # Another button stopbut = TkButton.new(fr) { text "Exit" background '#CCEECC' activebackground '#EEFFEE' foreground '#009900' command { exit } pack('side' => 'bottom', 'fill' => 'both') } # Switch button colors. def cswap(b1, b2) # Swap each color between the two buttons. for loc in ['background', 'foreground', 'activebackground'] c = b1.cget(loc) b1.configure(loc => b2.cget(loc)) b2.configure(loc => c) end end swapbut.configure( 'command' => proc { cswap(swapbut, stopbut) } ) Tk.mainloop

Inheritance

#!/usr/bin/ruby # Import the library. require 'tk' # Root window. root = TkRoot.new { title 'Push Me' background '#111188' } # Add a label to the root window. lab = TkLabel.new(root) { text "Hey there,\nPush a button!" background '#3333AA' foreground '#CCCCFF' } # Make it appear. lab.pack('side' => 'left', 'fill' => 'both') class TwoLabs < TkFrame # Switch button colors. def cswap # Swap each color between the two buttons. for loc in ['background', 'foreground', 'activebackground'] c = @swapbut.cget(loc) @swapbut.configure(loc => @stopbut.cget(loc)) @stopbut.configure(loc => c) end end def initialize super # Here's a button. I can't get the command setting to work # inside the block, since the self (apparently) becomes the TkButton, # not us. @swapbut = TkButton.new(self, 'command' => proc { self.cswap } ) { text "Swap" background '#EECCCC' activebackground '#FFEEEE' foreground '#990000' pack('side' => 'top', 'fill' => 'both') } # Another button @stopbut = TkButton.new(self) { text "Exit" background '#CCEECC' activebackground '#EEFFEE' foreground '#009900' command { exit } pack('side' => 'bottom', 'fill' => 'both') } end end # A frame can be used to arrange buttons with the packer. tl = TwoLabs.new tl.pack('side' => 'right', 'fill' => 'both') Tk.mainloop

Reflex Test

#!/usr/bin/ruby # Import the library. require 'tk' # Parameters. Width = 5 # Width of button grid. Height = 5 # Height of button grid. MinWait = 200 # Smallest button change wait (ms) MaxWait = 1400 # Largest button change wait (ms) InitWait = 800 # Initial button change wait (ms) LossRate = 2000 # Frequency to take away points. # Set defaults. Some we keep in constants to use later. BG = '#ccffcc' TkOption.add('*background', BG) TkOption.add('*activeBackground', '#ddffdd') FG = '#006600' TkOption.add('*foreground', FG) TkOption.add('*activeForeground', FG) TkOption.add('*troughColor', '#99dd99') # Root window. root = TkRoot.new('background' => BG) { title 'Click Fast' } # Button from the panel class PanelButton < TkButton private # Exchange colors on the button. def cswap for p in [['background', 'foreground'], ['activebackground', 'activeforeground']] c = cget(p[0]) configure(p[0] => cget(p[1])) configure(p[1] => c) end end public # Initialize the button within the widget sup, at position pos (zero-based) # with the number num. When pressed, send the score (+ or -) to cmd. # Scorekeeper is an object which implements an up and down methods to # receive score changes. def initialize(sup, pos, num, scorekeeper) super(sup, 'text' => num.to_s, 'command' => proc { self.pushed }, 'activeforeground' => '#990000', 'activebackground' => '#ffdddd') grid('row' => pos / Width + 1, 'column' => pos % Width, 'sticky' => 'news') @active = false @scorekeeper = scorekeeper end attr_reader :active # Activate or deactivate the button. def activate if not @active cswap @active = true end end def deactivate if @active cswap @active = false end end # When pushed, send our number, or negative our number, to the scorekeeping # command. def pushed n = self.cget('text').to_i if @active @scorekeeper.up(n) else @scorekeeper.down(n) end end end # This class calls reduces the score at the indicated time rate. class ScoreTimer # This object will call scorekeeper.down(step) each rate ms. def initialize(scorekeeper, rate = 500, step = 1) @scorekeeper = scorekeeper @rate = rate @step = step Tk.after(rate, proc { self.change }) end # Reduce the score periodically def change @scorekeeper.down(@step) Tk.after(@rate, proc { self.change }) end end # This is a box displaying a count-up timer in minutes and seconds to tenths # m:ss.d class TimeCounter < TkLabel # Initialize. Displays zero and starts the ticking event. def initialize(root) super(root, "text" => '0:00.0', 'anchor' => 'e') @count = 0 Tk.after(100, proc { self.change }) end # One clock tick (tenths of a second). Increment the counter, then build # the new display value. def change @count += 1 self.configure('text' => sprintf("%d:%02d.%d", @count / 600, (@count / 10) % 60, @count % 10)) Tk.after(100, proc { self.change }) end end # This is the main application GUI. class App private # Set the score value. def setscore(val) color = if val < 0 then 'red' else FG end @slab.configure('text' => val.to_s, 'foreground' => color) end public # The wait attribute is the amount of time (ms) between button changes. attr_writer :wait # Initialize it and have the applicate drawn in the root window. def initialize(root) # This is the label containing the score. Initially zero. @slab = TkLabel.new(root) { text "0" anchor 'e' grid('row' => 0, 'column' => 0, 'columnspan' => Width / 2, 'sticky' => 'w') } # This is the timer window at upper right. TimeCounter.new(root). grid('row' => 0, 'column' => Width/2, 'columnspan' => (Width+1)/2, 'sticky' => 'e') # Create the buttons. First, make an array of numbers from 1 to the # number of buttons, then create the buttons, each labelled with a # number chosen at random from the list, so thare are no repeats. nums = (1..Height*Width).to_a; @buts= [ ] for n in (0...Height*Width) pos = rand(nums.length) @buts.push(PanelButton.new(root, n, nums[pos], self)) nums.delete_at(pos) end # This creates the slider to adjust the speed of the game. The proc is # called whenever the slider changes, and is sent the new setting. scale = TkScale.new('command' => proc { |v| self.wait = v.to_i } ) { orient "horizontal" # Which way the slider goes. from MinWait # Value of smallest setting to MaxWait # Value of largest setting showvalue false # Don't show the numeric value of the setting. grid('row' => Height + 1, 'column' => 1, 'columnspan' => Width - 2, 'sticky' => 'news') } scale.set(InitWait) # Labels by the slider. TkLabel.new { text "Fast" anchor "w" grid("row" => Height + 1, 'column' => 0, 'sticky' => 'w') } TkLabel.new { text "Slow" anchor "e" grid("row" => Height + 1, 'column' => Width-1, 'sticky' => 'e') } @wait = InitWait # Decrement the score every LossRate period. @timer = ScoreTimer.new(self, LossRate) self.change end # Actions to increase or decrease the score. def up(delta) setscore(@slab.cget('text').to_i + delta) end def down(delta) setscore(@slab.cget('text').to_i - delta) end # Change (or set, if none is yet set) the active button. It deactivates # the button in @buts[0], It then chooses some other button at random, # activates that and swaps it into position 0. def change @buts[0].deactivate pos = rand(@buts.length - 1) + 1 @buts[0], @buts[pos] = @buts[pos], @buts[0] @buts[0].activate Tk.after(@wait, proc { self.change }) end end a = App.new(root) Tk.mainloop

Host Lookup

# # Host name lookup widget. # require 'tk' require 'socket' # Set colors. BG = '#AAAAFF' TkOption.add('*background', BG) TkOption.add('*activeBackground', '#CCCCFF') TkOption.add('*foreground', '#884400') # A label which does the needed lookup. class HostnameLabel < TkLabel # Look up host name in the assocated entry widget source, and display # in ourselves. def show hn = @source.get.strip if hn == '' ip = '' else begin ip = IPSocket.getaddress(hn) rescue ip = '[unknown]' end end configure('text' => ip) end # Create the widget, and bind the return key to run the lookup method (show). def initialize(root, entry) super(root, 'text' => '', 'width' => 15) @source = entry entry.bind('Return', proc { self.show }) end end # Root window root = TkRoot.new('background' => BG) { title 'Host Conversion' } # Title label tit = TkLabel.new { text "Host Name Conversion" relief 'groove' grid('row' => 0, 'column' => 0, 'columnspan' => 2, 'sticky' => 'news') } # Name entry. entr = TkEntry.new { width 25 grid('row' => 1, 'column' => 0, 'columnspan' => 2, 'sticky' => 'news') } dislab = nil # This needs to exist since we refer to it in the bind. entr.bind('Button-1', proc { |e| entr.delete(0,'end') dislab.configure('text' => '') }) # Reporting label. dislab = HostnameLabel.new(root, entr) dislab.grid('row' => 2, 'column' => 0, 'sticky' => 'news') # Go button. but = TkButton.new { text "Find" command { dislab.show } grid('row' => 2, 'column' => 1, 'sticky' => 'news') } Tk.mainloop

Bouncing Balls

#!/usr/bin/ruby # # This creates a simple animation of five balls bouncing around inside a # rectagle. Balls bounce off the sides, but pass through each other. Nothing # fancy. # # Import the library. require 'tk' # Dot diameter. Diameter = 10 # Update rate (ms). Frequency = 25 # Canvas size. Width = 400 Height = 300 # Set defaults. Some we keep in constants to use later. BG = '#ccccff' TkOption.add('*background', BG) # Root window. root = TkRoot.new('background' => BG) { title 'Bouncy, Bouncy' } # Canvas. c = TkCanvas.new(root) { width Width height Height pack('fill' => 'both') } # This is the circle that wanders around the canvas. class MovingCircle < TkcOval # Create with a moving circle on the canvas c with indicated color. def initialize(c, color) # Remember the canvas. @canv = c # Choose an initial location at random and create the object there. @xpos = rand(Width - Diameter) @ypos = rand(Height - Diameter) super(c, @xpos, @ypos, @xpos + Diameter, @ypos + Diameter, 'fill' => color) # Chose a velocity at random. 1 to 3 pixels per Frequency in each # dimension. @delx = (rand(3)+1)*(if rand(2) == 1 then 1 else -1 end) @dely = (rand(3)+1)*(if rand(2) == 1 then 1 else -1 end) # Start moving Tk.after(Frequency, proc { self.move } ) end # This adjusts a single dimension by one step, taking account of the # walls. Send a position, increment, and limit, and get back a new pos # and new increment (which might have its sign changed). def del(pos, inc, limit) # Move pos += inc # See if we hit the top or left, and reverse. if pos < 0 then pos = -pos inc = -inc # Likewise check for hitting the right or bottom elsif pos > limit - Diameter then pos = (limit - Diameter) - (pos - (limit - Diameter)) inc = -inc end # Send back the results. return pos, inc end # Move one step, then schedule the next move. def move # Remember current position, and compute the new one. oldx, oldy = @xpos, @ypos @xpos, @delx = del(@xpos, @delx, Width) @ypos, @dely = del(@ypos, @dely, Height) # Tell Tk about it. @canv.move(self, @xpos - oldx, @ypos - oldy) Tk.after(Frequency, proc { self.move } ) end end # Make several balls of different color. for color in [ '#FF9999', '#99FFFF', '#005588', '#992211', '#FF0055' ] MovingCircle.new(c, color) end Tk.mainloop

Binding and Actions

# # This shows a potential problem in providing button actions. The code blocks # are run in the context of the caller, but not until the button is pressed. # Therefore variable values are current as of the button press. This is not # always what you want. # require 'tk' # Root window root = TkRoot.new # Three buttons created in a loop, numbering from 1 to 3. bnum = 1 for fred in [ 17, 348, -48 ] TkButton.new(root) { text "Button " + bnum.to_s command proc { print "Button ", bnum, ", fred = ", fred, "\n" } grid('column' => 0, 'row' => bnum-1, 'sticky' => 'news') } bnum += 1 end # This holds and prints two values. The parms are evaluated when the # object is created, so we get the right values. The button command # will use any object which understands the call method. class Printer def initialize(num, fred) @num = num @fred = fred end def call print "Button ", @num, ", fred = ", @fred, "\n" end end # Three more buttons, but this time they use the Printer class and work # correctly. for fred in [ 'Dogbert', 97, 111 ] TkButton.new(root) { text "Button " + bnum.to_s command Printer.new(bnum, fred) grid('column' => 1, 'row' => bnum-4, 'sticky' => 'news') } bnum += 1 end # This is a more generic solution class Runner # Set a proc and some args. def initialize(method, *args) @method = method @args = args end # Run it with the args. def call @method.call(*@args) end end # Three more, but this time they work correctly. for fred in [ 86, 12, 123 ] TkButton.new(root) { text "Button " + bnum.to_s command Runner.new(proc { |n, f| print "Button ", n, ", fred = ", f, "\n" }, bnum, fred) grid('column' => 2, 'row' => bnum-7, 'sticky' => 'news') } bnum += 1 end # These are sort of button-like, but use binding. lnum = 1 for fred in [ 2973, 'Nosebleed', 349 ] b = TkLabel.new(root) { text "Label " + lnum.to_s grid('column' => 3, 'row' => lnum-1, 'sticky' => 'news') relief 'raised' background '#999999' padx 10 } b.bind('Button-1', proc { print "Label ", lnum, ", fred = ", fred, "\n" }) lnum += 1 end # This shows a nice feature of bind which allows extra parameters to be # sent in. for fred in [ 98733, 128, 'Norbert' ] b = TkLabel.new(root) { text "Label " + lnum.to_s grid('column' => 4, 'row' => lnum-4, 'sticky' => 'news') relief 'raised' background '#999999' padx 10 } b.bind('Button-1', proc { | n, f | print "Label ", n, ", fred = ", f, "\n" }, lnum, fred) lnum += 1 end Tk.mainloop

FTP Download Tool

#!/usr/bin/ruby require 'tk' require 'net/ftp' # Close the connection and terminate pgm. def term(conn) if conn begin conn.quit ensure conn.close end end exit end # Display an error dialog. def thud(title, message) Tk.messageBox('icon' => 'error', 'type' => 'ok', 'title' => title, 'message' => message) end # This is the login window. It pops up and asks for the remote host and the # user credentials, and a button to initiate the login when the fields are # ready. class LoginWindow # Generate s label/entry pair for the login window. These will be # appropriately gridded on row row inside par. Text box has width # width and places its contents into the reference $ref. If $ispwd, # treat it as a password entry box. Returns the text variable which # gives access to the entry. def genpair(row, text, width, ispwd=false) tbut = TkLabel.new(@main, 'text' => text) { grid('row' => row, 'column' => 0, 'sticky' => 'nse') } tvar = TkVariable.new('') lab = TkEntry.new(@main) { background 'white' foreground 'black' textvariable tvar width width grid('row' => row, 'column' => 1, 'sticky' => 'nsw') } lab.configure('show' => '*') if ispwd return tvar end # Log into the remote host. If successful, start the directory loader. # Modes are: 1: Anonymous, 2: User, 3: Return, which does anon if the # user infor was not filled in, and user otw. def do_login(mode) host = @host.value acct = @acct.value password = @password.value # Adjust user data by mode. if mode == 1 || (mode == 3 & acct == "" & password == "") acct ='anonymous' if password == "" password = 'anonymous' end end # Make sure we're all filled in. if host == "" || acct == "" || password == "" thud('No Login Info', "You must provide a hostname and login credentials.") return end # Attempt to connect to the remote host and log in begin @conn = Net::FTP.new(host, acct, password) @conn.passive = true rescue thud("Login Failed", $!) @conn = nil return end # Display the listing in the window. @listwin.setconn(@conn) @main.destroy() end def initialize(main, listwin, titfont, titcolor) @main = TkToplevel.new(main) @main.title('FTP Login') # Listing window. @listwin = listwin @conn = nil # This counts through the rows, which makes it easier to modify # the program. row = -1 # Label at the top of window. toplab = TkLabel.new(@main) { text "FTP Server Login" justify 'center' font titfont foreground titcolor grid('row' => (row += 1), 'column' => 0, 'columnspan' => 2, 'sticky' => 'news') } # Hostname entry @host = genpair(row += 1, 'Host:', 25) # Login buttons, in a frame for layout. bframe = TkFrame.new(@main) { grid('row' => (row += 1), 'column' => 0, 'columnspan' => 2, 'sticky' => 'news') } TkButton.new(bframe, 'command' => proc { self.do_login(1) }) { text 'Anon. Login' pack('side' => 'left', 'expand' => 'yes', 'fill' => 'both') } TkButton.new(bframe, 'command' => proc { self.do_login(2) }) { text 'User Login' pack('side' => 'left', 'expand' => 'yes', 'fill' => 'both') } # Login and password entries. @acct = genpair(row += 1, 'Login:', 15) @password = genpair(row += 1, 'Password:', 15, true) stop = TkButton.new(@main, 'command' => proc { term(@conn) } ) { text 'Exit' grid('row' => (row += 1), 'column' => 0, 'columnspan' => 2, 'sticky' => 'news') } # CR same as pushing login. @main.bind('Return', proc { self.do_login(3) }) end end # This is a window containing the file listing. class FileWindow < TkFrame def initialize(main) super # Set up the title appearance. titfont = 'arial 16 bold' titcolor = '#228800' @conn = nil # Label at top. TkLabel.new(self) { text 'FTP Download Agent' justify 'center' font titfont foreground titcolor pack('side' => 'top', 'fill' => 'x') } # Status label. @statuslab = TkLabel.new(self) { text 'Not Logged In' justify 'center' pack('side' => 'top', 'fill' => 'x') } # Exit button TkButton.new(self) { text 'Exit' command { term(@conn) } pack('side'=> 'bottom', 'fill' => 'x') } # List area with scroll bar. The list area is disabled since we # don't want the user to type into it. @listarea = TkText.new(self) { height 10 width 40 cursor 'sb_left_arrow' state 'disabled' pack('side' => 'left') yscrollbar(TkScrollbar.new).pack('side' => 'right', 'fill' => 'y') } # Bind the system exit button to our exit. main.protocol('WM_DELETE_WINDOW', proc { term(@conn) } ) # Create the login window. LoginWindow.new(main, self, titfont, titcolor) end # Change the color of a tag for entering and leaving. Unfortunately, there # is no active color for tags in a text box. def recolor(tag, color) @listarea.tag_configure(tag, 'foreground' => color) end # Do a CD and load the contents. If there is no directory name, skip # the CD. def load_dir(dir) if dir begin @conn.chdir(dir) rescue thud('No ' + dir, $!) end @statuslab.configure('text' => "[Loading " + dir + "]") else @statuslab.configure('text' => '[Loading Home Dir]') end update # Get the list of files. files = [ ] dirs = [ ] sawdots = false @conn.list() do |line| # Real lines start with the perm bits. And we don't want specials. if line =~ /^[\-d]([r\-][w\-][x\-]){3}/ # Extract the useful parts, toss the bones. The limit keeps us from # dividing file names containing spaces. parts = line.split(/\s+/, 9) if parts.length >= 9 fn = parts.pop() sawdots = true if fn == '..' if parts[0][0..0] == 'd' dirs.push(fn) else files.push(fn) end end end end # Add .. if not present, then sort the list. dirs.push('..') unless sawdots files.sort! dirs.sort! # Clear the old contents from the directory listing box. @listarea.configure('state' => 'normal') @listarea.delete('1.0', 'end') # Fill in the directories. Bind for directory load (us). ct = 0 while fn = dirs.shift tagname = "fn" + ct.to_s @listarea.insert('end', fn+"\n", tagname) @listarea.tag_configure(tagname, 'foreground' => '#4444FF') @listarea.tag_bind(tagname, 'Button-1', proc { |f| self.load_dir(f) }, fn) @listarea.tag_bind(tagname, 'Enter', proc { |t| self.recolor(t, '#0000aa') }, tagname) @listarea.tag_bind(tagname, 'Leave', proc { |t| self.recolor(t, '#4444ff') }, tagname) ct += 1 end # Fill in the files. Bind for download. while fn = files.shift tagname = "fn" + ct.to_s @listarea.insert('end', fn+"\n", tagname) @listarea.tag_configure(tagname, 'foreground' => 'red') @listarea.tag_bind(tagname, 'Button-1', proc { |f| self.dld_file(f) }, fn) @listarea.tag_bind(tagname, 'Enter', proc { |t| self.recolor(t, '#880000') }, tagname) @listarea.tag_bind(tagname, 'Leave', proc { |t| self.recolor(t, 'red') }, tagname) ct += 1 end # Lock it up so the user can't mess with it. @listarea.configure('state' => 'disabled') # Update the status label. begin loc = @conn.pwd() rescue thud('PWD Failed', $!) loc = '???' end @statuslab.configure('text' => loc) end # Download the file. def dld_file(fn) # Announce. @statuslab.configure('text' => "[Retrieving " + fn + "]") update # Get the file. begin @conn.getbinaryfile(fn) rescue thud('DLD Failed', fn + ': ' + $!) @statuslab.configure('text' => '') else @statuslab.configure('text' => 'Got ' + fn) end end # This is a hook that the login window calls after a successful login. # The login window makes the connection and attempts to login. When this # succeeds, it calls setconn() and destroys itself. Setconn records the # connection (which the login box created), then does the initial # directory load. def setconn(conn) @conn = conn load_dir(nil) end end # Create the main window, set the default colors, create the GUI, then # fire the sucker up. BG = '#E6E6FA' root = TkRoot.new('background' => BG) { title "FTP Download" } TkOption.add("*background", BG) TkOption.add("*activebackground", '#FFE6FA') TkOption.add("*foreground", '#0000FF') TkOption.add("*activeforeground", '#0000FF') FileWindow.new(root).pack() Tk.mainloop

Building a 30 line Ruby HTTP server

How HTTP and TCP work together
A basic HTTP GET request
A Minimal Ruby HTTP server
Serving a Rack app
Reading requests


How HTTP and TCP work together

TCP is a transport protocol that describes how a server and a client exchange data. HTTP is a request-response protocol that specifically describes how web servers exchange data with HTTP clients or web browsers. HTTP commonly uses TCP as its transport protocol. In essence, an HTTP server is a TCP server that "speaks" HTTP. # tcp_server.rb require 'socket' server = TCPServer.new 5678 while session = server.accept session.puts "Hello world! The time is #{Time.now}" session.close end In this example of a TCP server, the server binds to port 5678 and waits for a client to connect. When that happens, it sends a message to the client, and then closes the connection. After it's done talking to the first client, the server waits for another client to connect to send its message to again. # tcp_client.rb require 'socket' server = TCPSocket.new 'localhost', 5678 while line = server.gets puts line end server.close To connect to our server, we'll need a TCP client. This example client connects to the same port (5678) and uses server.gets to receive data from the server, which is then printed. When it stops receiving data, it closes the connection to the server and the program will exit. When you start the server server is running ($ ruby tcp_server.rb), you can start the client in a separate tab to receive the server's message. shell $ ruby tcp_client.rb Hello world! The time is 2016-11-23 15:17:11 +0100 $ With a bit of imagination, our TCP server and client work somewhat like a web server and a browser. The client sends a request, the server responds, and the connection is closed. That's how the request-response pattern works, which is exactly what we need to build an HTTP server. Before we get to the good part, let's look at what HTTP requests and responses look like.

A basic HTTP GET request

The most basic HTTP GET request is a request-line without any additional headers or a request body. shell GET / HTTP/1.1\r\n The Request-Line consists of four parts: A method token (GET, in this example) The Request-URI (/) The protocol version (HTTP/1.1) A CRLF (a carriage return: \r, followed by line feed: \n) to indicate the end of the line The server will respond with an HTTP response, which may look like this: shell HTTP/1.1 200\r\nContent-Type: text/html\r\n\r\n\Hello world! This response consists of: A status line: the protocol version ("HTTP/1.1"), followed by a space, the response's status code ("200"), and terminated with a CRLF (\r\n) Optional header lines. In this case, there's only one header line ("Content-Type: text/html"), but there could be multiple (separated with with a CRLF: \r\n) A newline (or a double CRLF) to separate the status line and header from the body: (\r\n\r\n) The body: "Hello world!"

A Minimal Ruby HTTP server

Enough talk. Now that we know how to create a TCP server in Ruby and what some HTTP requests and responses look like, we can build a minimal HTTP server. You'll notice that the web server looks mostly the same as the TCP server we discussed earlier. The general idea is the same, we're just using the HTTP protocol to format our message. Also, because we'll use a browser to send requests and parse responses, we won't have to implement a client this time. # http_server.rb require 'socket' server = TCPServer.new 5678 while session = server.accept request = session.gets puts request session.print "HTTP/1.1 200\r\n" # 1 session.print "Content-Type: text/html\r\n" # 2 session.print "\r\n" # 3 session.print "Hello world! The time is #{Time.now}" #4 session.close end After the server receives a request, like before, it uses session.print to send a message back to the client: Instead of just our message, it prefixes the response with a status line, a header and a newline: The status line (HTTP 1.1 200\r\n) to tell the browser that the HTTP version is 1.1 and the response code is "200" A header to indicate that the response has a text/html content type (Content-Type: text/html\r\n) The newline (\r\n) The body: "Hello world! …" Like before, it closes the connection after sending the message. We're not reading the request yet, so it just prints it to the console for now. If you start the server and open http://localhost:5678 in your browser, you should see the "Hello world! …"-line with the current time, like we received from our TCP client earlier.

Serving a Rack app

Until now, our server has been returning a single response for each request. To make it a little more useful, we could add more responses to our server. Instead of adding these to the server directly, we'll use a Rack app. Our server will parse HTTP requests and pass them to the Rack app, which will then return a response for the server to send back to the client. Rack is an interface between web servers that support Ruby and most Ruby web frameworks like Rails and Sinatra. In its simplest form, a Rack app is an object that responds to call and returns a "tiplet", an array with three items: an HTTP response code, a hash of HTTP headers and a body. app = Proc.new do |env| ['200', {'Content-Type' => 'text/html'}, ["Hello world! The time is #{Time.now}"]] end In this example, the response code is "200", we're passing "text/html" as the content type through the headers, and the body is an array with a string. To allow our server to serve responses from this app, we'll need to turn the returned triplet into a HTTP response string. Instead of always returning a static response, like we did before, we'll now have to build the response from the triplet returned by the Rack app. # http_server.rb require 'socket' app = Proc.new do ['200', {'Content-Type' => 'text/html'}, ["Hello world! The time is #{Time.now}"]] end server = TCPServer.new 5678 while session = server.accept request = session.gets puts request # 1 status, headers, body = app.call({}) # 2 session.print "HTTP/1.1 #{status}\r\n" # 3 headers.each do |key, value| session.print "#{key}: #{value}\r\n" end # 4 session.print "\r\n" # 5 body.each do |part| session.print part end session.close end To serve the response we've received from the Rack app, there's some changes we'll make to our server: Get the status code, headers, and body from the triplet returned by app.call. Use the status code to build the status line Loop over the headers and add a header line for each key-value pair in the hash Print a newline to separate the status line and headers from the body Loop over the body and print each part. Since there's only one part in our body array, it'll simply print our "Hello world"-message to the session before closing it.

Reading requests

Until now, our server has been ignoring the request variable. We didn't need to as our Rack app always returned the same response. Rack::Lobster is an example app that ships with Rack and uses request URL parameters in order to function. Instead of the Proc we used as an app before, we'll use that as our testing app from now on. # http_server.rb require 'socket' require 'rack' require 'rack/lobster' app = Rack::Lobster.new server = TCPServer.new 5678 while session = server.accept # ... Opening the browser will now show a lobster instead of the boring string it printed before. Lobstericious! The "flip!" and "crash!" links link to /?flip=left and /?flip=crash respectively. However, when following the links, the lobster doesn't flip and nothing crashes just yet. That's because our server doesn't handle query strings right now. Remember the request variable we ignored before? If we look at our server's logs, we'll see the request strings for each of the pages. shell GET / HTTP/1.1 GET /?flip=left HTTP/1.1 GET /?flip=crash HTTP/1.1 The HTTP request strings include the request method ("GET"), the request path (/, /?flip=left and /?flip=crash), and the HTTP version. We can use this information to determine what we need to serve. # http_server.rb require 'socket' require 'rack' require 'rack/lobster' app = Rack::Lobster.new server = TCPServer.new 5678 while session = server.accept request = session.gets puts request # 1 method, full_path = request.split(' ') # 2 path, query = full_path.split('?') # 3 status, headers, body = app.call({ 'REQUEST_METHOD' => method, 'PATH_INFO' => path, 'QUERY_STRING' => query }) session.print "HTTP/1.1 #{status}\r\n" headers.each do |key, value| session.print "#{key}: #{value}\r\n" end session.print "\r\n" body.each do |part| session.print part end session.close end To parse the request and send the request parameters to the Rack app, we'll split the request string up and send it to the Rack app: Split the request string into a method and a full path Split the full path into a path and a query Pass those to our app in a Rack environment hash. For example, a request like GET /?flip=left HTTP/1.1\r\n will be passed to the app like this: { 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/', 'QUERY_STRING' => '?flip=left' } Restarting our server, visiting http://localhost:5678, and clicking the "flip!"-link will now flip the lobster, and clicking the "crash!" link will crash our web server. We've just scratched the surface of implementing a HTTP server, and ours is only 30 lines of code, but it explains the basic idea. It accepts GET requests, passes the request's attributes to a Rack app, and sends back responses to the browser. Although it doesn't handle things like request streaming and POST requests, our server could theoretically be used to serve other Rack apps too.